package org.izao;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LoggingHandler;
import io.netty.util.concurrent.EventExecutor;
import io.netty.util.concurrent.GlobalEventExecutor;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


/**
 * Created by Administrator on 2015/1/20 0020.
 */
public class IZaoClient {

    public enum Status {
        IDLE,
        Connecting, //链接状态
        Connected,  //链接
        DisConneted //断开
    }

    public static final org.apache.log4j.Logger LOGGER = org.apache.log4j.Logger.getLogger(IZaoClient.class);

    /**
     * 默认重连次数
     */
    public static final int DEFAULT_RETRY_COUNT = 5;

    /**
     * 默认单次连接超时次数
     */
    public static final int DEFAULT_CONNECT_TIME_OUT = 10000;

    /**
     * 链接状态
     */
    private volatile Status status = Status.IDLE;


    /**
     * 主机地址
     */
    private String hostname;

    /**
     * Tcp Server 开放端口
     */
    private int port;

    /**
     * 请求编号
     */
    private int reqId;
    /**
     * 请求编号<->回调函数
     */
    private Map<Integer, DataCallBack> cbs;
    /**
     * 监听器
     */
    private Map<String, List<DataListener>> listeners;

    //netty client Bootstrap
    private Bootstrap nettyBootstrap;


    //Tcp channel
    private Channel tcpChannel;

    public IZaoClient(String hostname, int port) {


        //初始化监听器容器
        listeners = new HashMap<String, List<DataListener>>();

        cbs = new HashMap<Integer, DataCallBack>();
        this.hostname = hostname;
        this.port = port;
        initSocket();
    }

    /**
     * 初始化连接相关事项
     */
    private void initSocket() {
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        this.nettyBootstrap = new Bootstrap();
        nettyBootstrap.group(workerGroup);

        this.nettyBootstrap.option(ChannelOption.SO_KEEPALIVE, true);
        this.nettyBootstrap.option(ChannelOption.TCP_NODELAY, true);
//        this.nettyBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000); // 10s
        this.nettyBootstrap.channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                ChannelPipeline pipeline = ch.pipeline();
        /*
         * 这个地方的 必须和服务端对应上。否则无法正常解码和编码
         *
         * 解码和编码 我将会在下一张为大家详细的讲解。再次暂时不做详细的描述
         *
         **/
                pipeline.addLast("framer", new DelimiterBasedFrameDecoder(Integer.MAX_VALUE, Delimiters.lineDelimiter()));
                pipeline.addLast("decoder", new StringDecoder());
                pipeline.addLast("encoder", new StringEncoder());

                pipeline.addLast("loggingHandler", new LoggingHandler());

                EventExecutor eventExecutor = GlobalEventExecutor.INSTANCE;
                // 客户端的逻辑
                pipeline.addLast(eventExecutor, new SimpleChannelInboundHandler<String>() {
                    @Override
                    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
                        LOGGER.info(">>>>>Recv data:" + msg);
                        JSONObject jsonObject = null;
                        try {
                            jsonObject = new JSONObject(msg);
                            if (!jsonObject.has("route")) {
                                return;
                            }
                            String route = jsonObject.getString("route");
                            jsonObject.remove("route");
                            //获取该请求Id
                            if (jsonObject.has("reqId")) {
                                int reqId = jsonObject.getInt("reqId");
                                if (cbs.get(reqId) != null) {
                                    DataCallBack dataCallBack = cbs.get(reqId);
                                    dataCallBack.responseData(jsonObject);
                                }
                            }
                            emit(route, jsonObject);
                        } catch (JSONException e) {
                            jsonObject = new JSONObject();
                            jsonObject.put("msg", e.getMessage());
                            emit("error", jsonObject);
                        }

                    }

                    @Override
                    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
                        cause.printStackTrace();
                        LOGGER.warn("exceptionCaught:" + cause.getLocalizedMessage());
                    }

                    @Override
                    public void channelActive(ChannelHandlerContext ctx) throws Exception {
                        super.channelActive(ctx);
                        LOGGER.info("Client active ");
                        emit("connect", null);
                    }

                    @Override
                    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
                        super.channelInactive(ctx);
                        LOGGER.info("Client close ");
                        IZaoClient.this.status = IZaoClient.Status.DisConneted;
                        emit("disconnect", null);
                        tcpChannel = null;
                    }
                });
            }
        });

    }

    /**
     * 连接服务器，使用默认的重连次数和连接超时时间
     *
     * @return 是否连接成功
     */
    public boolean connect() {
        return this.connect(DEFAULT_CONNECT_TIME_OUT, DEFAULT_RETRY_COUNT);
    }

    /**
     * 连接服务器
     *
     * @param timeout    连接超时时间
     * @param retryCount 重连次数
     * @return 是否连接成功
     */
    public boolean connect(int timeout, int retryCount) {
        //设置为正在连接状态
        this.status = Status.Connecting;

        if (timeout < 0) {
            timeout = DEFAULT_CONNECT_TIME_OUT;
        }
        if (timeout < 0) {
            retryCount = DEFAULT_RETRY_COUNT;
        }
        boolean isOk = false;
        // 连接服务端
        int tryIndex = -1;
        while (tryIndex < retryCount) {
            tryIndex++;
            try {
                this.nettyBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, timeout); // 10s
                ChannelFuture channelFuture = nettyBootstrap.connect(hostname, port).sync();
                this.tcpChannel = channelFuture.channel();
                isOk = true;
                break;
            } catch (Exception e) {
                e.printStackTrace();
                System.err.println("第" + (tryIndex + 1) + "次尝试连接服务器失败");
                JSONObject jsonObject = new JSONObject();
                try {
                    jsonObject.put("msg", "连接失败:" + e.getMessage());
                } catch (JSONException e1) {
                    e1.printStackTrace();
                }
                emit("connectFail", jsonObject);
            }
        }
        if (!isOk) {
            status = Status.DisConneted;
        }
        return isOk;
    }

    /**
     * 从服务器断开
     */
    public void disconnect() {
        if (this.tcpChannel == null) {
            this.tcpChannel.disconnect();
        }
    }

    /**
     * 退出，释放线程等相关资源
     */
    public void release() {
        //退出，释放线程等相关资源
        nettyBootstrap.group().shutdownGracefully();
    }

    /**
     * Send message to the server side.
     *
     * @param reqId requestWithArgs id
     * @param route requestWithArgs route
     * @param msg   request message
     */
    private void sendMessage(int reqId, String route, JSONObject msg) {
        if (this.tcpChannel == null) {
            LOGGER.warn("tcpChannel is null, can not send data");
            LOGGER.warn("tcpChannel is null, can not send data,curr status:" + this.status);
            if (this.status != Status.Connecting && this.status != Status.IDLE) {
                this.status = Status.DisConneted;
            }
            return;
        }
        String content = Protocol.encode(reqId, route, msg);
        if (this.tcpChannel.isWritable()) {
            try {
                content = new String(content.getBytes(),"UTF-8");
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            this.tcpChannel.writeAndFlush(content);
            this.tcpChannel.flush();
        }
    }

    /**
     * 直接服务器发送小心
     *
     * @param content
     */
    public void sendMessage(String content) {
        if (this.tcpChannel == null) {
            LOGGER.warn("tcpChannel is null, can not send data,curr status:" + this.status);
            if (this.status != Status.Connecting && this.status != Status.IDLE) {
                this.status = Status.DisConneted;
            }
            return;
        }
        if (!content.endsWith("\r\n")) {
            content = content + "\r\n";
        }
        if (this.tcpChannel.isWritable()) {
            try {
                content = new String(content.getBytes(),"UTF-8");
                LOGGER.debug(content);
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            this.tcpChannel.writeAndFlush(content);
            this.tcpChannel.flush();
        }
    }

    /**
     * Notify the server without response
     *
     * @param route
     * @param msg
     */
    public void inform(String route, JSONObject msg) {
        requestWithArgs(route, msg);
    }

    /**
     * @param route
     * @param jsonObject
     * @param dataCallBack
     */
    public void request(String route, JSONObject jsonObject, DataCallBack dataCallBack) {
        this.requestWithArgs(route, jsonObject, dataCallBack);
    }

    /**
     * Client send requestWithArgs to the server and get response data.
     *
     * @param args
     */
    private void requestWithArgs(Object... args) {
        if (args.length < 2 || args.length > 3) {
            throw new RuntimeException("the requestWithArgs arguments is error.");
        }
        // first argument must be string
        if (!(args[0] instanceof String)) {
            throw new RuntimeException("the route of requestWithArgs is error.");
        }

        String route = args[0].toString();
        JSONObject msg = null;
        DataCallBack cb = null;

        if (args.length == 2) {
            if (args[1] instanceof JSONObject) {
                msg = (JSONObject) args[1];
            } else if (args[1] instanceof DataCallBack) {
                cb = (DataCallBack) args[1];
            }
        } else {
            msg = (JSONObject) args[1];
            cb = (DataCallBack) args[2];
        }
        msg = filter(msg);
        reqId++;
        cbs.put(reqId, cb);
        sendMessage(reqId, route, msg);
    }

    /**
     * Add event listener and wait for broadcast message.
     *
     * @param route
     * @param listener
     */
    public void on(String route, DataListener listener) {
        List<DataListener> list = listeners.get(route);
        if (list == null) {
            list = new ArrayList<DataListener>();
        }
        list.add(listener);
        listeners.put(route, list);
    }

    /**
     * Add timestamp to message.
     *
     * @param msg
     * @return msg
     */
    private JSONObject filter(JSONObject msg) {
        if (msg == null) {
            msg = new JSONObject();
        }
        long date = System.currentTimeMillis();
        try {
            msg.put("timestamp", date);
        } catch (JSONException e) {
            e.printStackTrace();
        }
        return msg;
    }

    /**
     * Touch off the event and call listeners corresponding route.
     *
     * @param route
     * @param message
     * @return true if call success, false if there is no listeners for this
     * route.
     */
    private void emit(String route, JSONObject message) {
        List<DataListener> list = listeners.get(route);
        if (list == null) {
            LOGGER.warn("there is no listeners for route:[" + route + "]");
            return;
        }
        for (DataListener listener : list) {
            DataEvent event = new DataEvent(this, message);
            listener.receiveData(event);
        }
    }

    public Status getStatus() {
        return status;
    }

    public void setStatus(Status status) {
        this.status = status;
    }
}
